﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace NamedBinaryTag
{
	public sealed class TagReader : MarshalByRefObject, IDisposable
	{
		private Stack<TagReaderContext> previousContext;
		private TagReaderContext currentContext;
		private bool atStart;
	
		public byte TagType
		{
			get
			{
				if (atStart)
					throw new InvalidOperationException("Not initialized");
				return currentContext.TagType;
			}
		}

		public string Name
		{
			get
			{
				return currentContext.Name;
			}
		}

		public bool InList
		{
			get
			{
				return ((previousContext.Count > 0) && (previousContext.Peek().TagType == 9));
			}
		}

		public bool InCompound
		{
			get
			{
				return ((previousContext.Count > 0) && (previousContext.Peek().TagType == 10));
			}
		}

		public int Count
		{
			get
			{
				if (currentContext.TagType == 9) 
					return currentContext.Count;
				throw new InvalidOperationException("Not a list.");
			}
		}

		public bool IsRoot
		{
			get
			{
				if (atStart)
					throw new InvalidOperationException("Not initialized");
				return previousContext.Count == 0;
			}
		}

		public bool IsContainer
		{
			get
			{
				return (currentContext.TagType == 9) || (currentContext.TagType == 10);
			}
		}

		internal System.IO.Stream BaseStream { get; private set; }
		internal System.IO.BinaryReader BaseStreamReader { get; private set; }

		public byte ListTagType
		{
			get
			{
				if (currentContext.TagType == 9)
					return currentContext.ListTagType;
				throw new InvalidOperationException("Not a list.");
			}
		}

		public int Index
		{
			get
			{
				if (InList)
					return currentContext.Index;
				throw new InvalidOperationException("Not in a list.");
			}
		}

		#region IDisposable Members

		public void Dispose()
		{
			previousContext.Clear();
			currentContext = new TagReaderContext();
			BaseStreamReader.Dispose();
		}

		#endregion

		private TagReader()
		{
			atStart = true;
		}

		public static TagReader Create(System.IO.Stream source)
		{
			return Create(source, false);
		}

		public static TagReader Create(System.IO.Stream source, bool randomAccess)
		{
			if (randomAccess && source.CanSeek)
				throw new InvalidOperationException("Not seekable and asked for.");
			TagReader reader = new TagReader();
			reader.BaseStream = source;
			reader.BaseStreamReader = new System.IO.BinaryReader(source, Encoding.UTF8);
			reader.previousContext = new Stack<TagReaderContext>();
			return reader;
		}

		public static System.IO.Stream Prepare(System.IO.Stream source)
		{
			if (!source.CanSeek)
				source = TagHelper.WrapHeadered(source);
			CompressionType type = TagHelper.DetermineCompression(source);
			switch (type)
			{
				case CompressionType.GZip:
					return TagHelper.WrapGZip(source);
				case CompressionType.Zlib:
					return TagHelper.WrapZLib(source);
			}
			return source;
		}

		public static System.IO.Stream Prepare(System.IO.Stream source, CompressionType compression)
		{
			if (!source.CanSeek)
				source = TagHelper.WrapHeadered(source);
			switch (compression)
			{
				case CompressionType.GZip:
					return TagHelper.WrapGZip(source);
				case CompressionType.Zlib:
					return TagHelper.WrapZLib(source);
			}
			return source;
		}

		public T GetValue<T>()
		{
			if (typeof(T) == currentContext.Value.GetType())
				return (T)currentContext.Value;
			throw new InvalidOperationException("Generic type does not match Value type.");
		}

		public bool SelectItem(int index)
		{
			if (!InList)
				throw new InvalidOperationException();
			if ((currentContext.Index > index) && !BaseStream.CanSeek)
				throw new InvalidOperationException("Could not access to a previous element");
			if (currentContext.Index == index)
				return true;
			if (currentContext.Index < index)
			{
				if (index >= previousContext.Peek().Count)
					return false;
				while (currentContext.Index < index)
				{
					SkipElements();
					MoveNext();
				}
			}
			else
			{
				MoveToParent();
				MoveToFirstElement();
				return SelectItem(index);
			}
			return true;
		}

		public bool SelectItem(string name)
		{
			if (!InCompound)
				throw new InvalidOperationException();
			if (!BaseStream.CanSeek)
				throw new InvalidOperationException();
			MoveToParent();
			MoveToFirstElement();
			while ((currentContext.TagType != 0) &&
				(currentContext.Name != name))
			{
				SkipElements();
				MoveNext();
			}
			return currentContext.TagType != 0;
		}

		public string[] GetKeys()
		{
			if (!InCompound)
				throw new InvalidOperationException();
			if (!BaseStream.CanSeek)
				throw new InvalidOperationException();
			MoveToParent();
			MoveToFirstElement();
			List<string> keys = new List<string>();
			while (currentContext.TagType != 0)
			{
				keys.Add(currentContext.Name);
				SkipElements();
				MoveNext();
			}
			return keys.ToArray();
		}

		public bool ContainsKey(string name)
		{
			return SelectItem(name);
		}

		public bool MoveToFirstElement()
		{
			if ((currentContext.TagType != 10) &&
				((currentContext.TagType != 9) || (currentContext.Count == 0)))
				return false;
			return MoveNext();
		}

		public void MoveToParent()
		{
			if (!BaseStream.CanSeek)
				throw new InvalidOperationException();
			if (IsRoot)
				throw new InvalidOperationException("No such thing as a parent.");
			if (previousContext.Peek().Position == -1)
				throw new InvalidOperationException("Invalid position");
			currentContext = previousContext.Pop();
			long newPosition = currentContext.Position;
			if (!InList) // If there is the tag type byte.
			{
				newPosition++;
			}
			if (InCompound || IsRoot) // If there is a name for the tag.
			{
				newPosition+=2;
				newPosition += Encoding.UTF8.GetByteCount(currentContext.Name);
			}
			if (currentContext.TagType == 9)
			{
				newPosition += 5; // The list tags type and the number of elements.
			}
			BaseStream.Position = newPosition;
		}

		public bool MoveNext()
		{
			if (atStart)
				return ReadRoot();
			if (IsRoot && currentContext.TagType == 0)
				return false;
			return ReadNext();
		}

		private bool ReadRoot()
		{
			atStart = false;
			return ReadNextNamedElement();
		}

		private bool ReadNext()
		{
			switch (currentContext.TagType)
			{
				case 9:
					if (currentContext.Count > 0)
					{
						previousContext.Push(currentContext);
						return ReadFirstListElement();
					}
					return ReadNextElement();
				case 10:
					previousContext.Push(currentContext);
					return ReadNextNamedElement();
				case 0:
					return ReadNextParentElement();
				default:
					return ReadNextElement();
			}
		}

		private bool ReadFirstListElement()
		{
			TagReaderContext context = new TagReaderContext();
			context.Position = GetStreamPosition();
			context.TagType = previousContext.Peek().ListTagType;
			return ReadNextElementData(context);
		}

		private bool ReadNextListElement(TagReaderContext lastContext)
		{
			TagReaderContext context = new TagReaderContext();
			context.Position = GetStreamPosition();
			context.TagType = lastContext.TagType;
			context.Index = lastContext.Index + 1;
			return ReadNextElementData(context);
		}

		private bool ReadNextNamedElement()
		{
			TagReaderContext context = new TagReaderContext();
			context.Position = GetStreamPosition();
			context.TagType = BaseStreamReader.ReadByte();
			if (context.TagType != 0)
			{
				short length = ByteReverser.Reverse(BaseStreamReader.ReadInt16());
				context.Name = new string(Encoding.UTF8.GetChars(BaseStreamReader.ReadBytes(length)));
			}
			else
				context.Name = string.Empty;
			return ReadNextElementData(context);
		}

		private long GetStreamPosition()
		{
			if (!BaseStream.CanSeek)
				return -1;
			try
			{
				return BaseStream.Position;
			}
			catch (NotSupportedException)
			{
				return -1;
			}
		}

		private bool ReadNextElementData(TagReaderContext context)
		{
			switch (context.TagType)
			{
				case 0:
					break;
				case 1:
					context.Value = BaseStreamReader.ReadByte();
					break;
				case 2:
					context.Value = ByteReverser.Reverse(BaseStreamReader.ReadInt16());
					break;
				case 3:
					context.Value = ByteReverser.Reverse(BaseStreamReader.ReadInt32());
					break;
				case 4:
					context.Value = ByteReverser.Reverse(BaseStreamReader.ReadInt64());
					break;
				case 5:
					context.Value = ByteReverser.Reverse(BaseStreamReader.ReadSingle());
					break;
				case 6:
					context.Value = ByteReverser.Reverse(BaseStreamReader.ReadDouble());
					break;
				case 7:
					int length = ByteReverser.Reverse(BaseStreamReader.ReadInt32());
					context.Value = BaseStreamReader.ReadBytes(length);
					break;
				case 8:
					short slength = ByteReverser.Reverse(BaseStreamReader.ReadInt16());
					context.Value = new string(Encoding.UTF8.GetChars(BaseStreamReader.ReadBytes(slength)));
					break;
				case 9:
					context.ListTagType = BaseStreamReader.ReadByte();
					context.Count = ByteReverser.Reverse(BaseStreamReader.ReadInt32());
					break;
				case 10:
					break;
				default:
					return false;
			}
			currentContext = context;
			return true;
		}

		private bool ReadNextParentElement()
		{
			TagReaderContext lastContext = previousContext.Pop();
			return ReadNextElement(lastContext);
		}

		private bool ReadNextElement()
		{
			return ReadNextElement(currentContext);
		}

		private bool ReadNextElement(TagReaderContext lastContext)
		{
			if (InList)
			{
				if (lastContext.Index + 1 < previousContext.Peek().Count)
					return ReadNextListElement(lastContext);
				return ReadNextParentElement();
			}
			if (InCompound || IsRoot)
			{
				return ReadNextNamedElement();
			}
			return false;
		}

		public void SkipElements()
		{
			if (currentContext.TagType == 9)
			{
				if (currentContext.Count > 0)
				{
					int count = currentContext.Count;
					for (int i = 0; i < count; i++)
					{
						if (!MoveNext())
							return;
						if ((currentContext.TagType == 9) ||
							(currentContext.TagType == 10))
							SkipElements();
					}
				}
			}
			if (currentContext.TagType == 10)
			{
				while (MoveNext())
				{
					if (currentContext.TagType == 0)
						break;
					if ((currentContext.TagType == 9) ||
						(currentContext.TagType == 10))
						SkipElements();
				}
			}
		}
	}
}
